
"""
Kiss python wrapper for amcharts
"""

# XXX could add validatation to make sure xml attributes make sense, etcetera.

#from whiff.rdjson import jsonParse
import types
from whiff.rdjson import jsonParse

class Chart:
    "encapsulates chart information"
    def init_settings(self, settings=None, labels=None, guides=None):
        if settings is None:
            settings = {}
        self.settings = settings
        if labels is None:
            labels = []
        self.labels = labels
        # guides are only relevant to line/bar charts...
        if guides is None:
            guides = []
        self.guides = guides
    def set(self, settingName, settingValue):
        self.settings[settingName] = settingValue
    def settingsXML(self):
        st = settingsTree(self.settings)
        return formatSettingsTree("settings", st, self.labels, self.guides)
    def rectify(self):
        "for overloading"
        pass
    def addGuide(self, guide):
        self.guides.append(guide)
    def addLabel(self, label):
        label.lid = repr(len(self.labels))
        self.labels.append(label)
    def labelsFromDict(self, dictionary, env, prefix):
        labelDefaults = getEnvironmentParameters(prefix+"label.", None, env)
        labels = dictionary.get("labels", ())
        for l in labels:
            if type(l) is not types.DictType:
                l = {"text": l}
            l = addDefaults(l, labelDefaults)
            lab = Label(l)
            self.addLabel(lab)
    def guidesFromDict(self, dictionary, env, prefix):
        # xxxx very similar to labelsFromDict...
        # XXXX I DON'T KNOW HOW TO HANDLE MAX_MIN AT THE MOMENT
        guideDefaults = getEnvironmentParameters(prefix+"guide.", None, env)
        guides = dictionary.get("guides", ())
        for g in guides:
            if type(g) is not types.DictType:
                g = {"start_value": g}
            g = addDefaults(g, guideDefaults)
            gd = Guide(g)
            self.addGuide(gd)
    def specifications(self, jsVariable, flashId, flashName, width, height,
                     swfFile = None,
                     path = None,
                     swfObjectJs = None,
                     flashVersion = None,
                     backgroundColor = None):
        if swfFile is None:
            swfFile = self.default_swfFile
        if path is None:
            path = self.default_path
        if swfObjectJs is None:
            swfObjectJs = self.default_swfObjectJs
        if flashVersion is None:
            flashVersion = "8"
        if backgroundColor is None:
            backgroundColor = "#FFFFFF"
        self.jsVariable = jsVariable
        self.flashId = flashId
        self.flashName = flashName
        self.width = width
        self.height = height
        self.swfFile = swfFile
        self.path = path
        self.swfObjectJs = swfObjectJs
        self.flashVersion = flashVersion
        self.backgroundColor = backgroundColor
    def htmlFragment(self):
        D = {}
        D["JSVARIABLE"] = self.jsVariable
        D["DATA"] = jsonParse.format(self.dataXML())
        D["SETTINGS"] = jsonParse.format(self.settingsXML())
        D["SWFFILE"] = self.swfFile
        D["PATH"] = self.path
        D["SWFOBJECTJS"] = self.swfObjectJs
        D["FLASHID"] = self.flashId
        D["FLASHNAME"] = self.flashName
        D["WIDTH"] = self.width
        D["HEIGHT"] = self.height
        D["FLASHVERSION"] = self.flashVersion
        D["BACKGROUNDCOLOR"] = self.backgroundColor
        result = HTML_FRAGMENT_TEMPLATE % D
        return result
    def swfObjectScript(self):
        return '<script type="text/javascript" src="%s"></script>' % self.swfObjectJs
    def dataJs(self):
        data = jsonParse.format(self.dataXML())
        return '%s.addVariable("chart_data", encodeURIComponent(%s));' % (self.jsVariable, data)
    def settingsJs(self):
        settings = jsonParse.format(self.settingsXML())
        return '%(JSVARIABLE)s.addVariable("chart_settings", encodeURIComponent(%(SETTINGS)s));' % (self.jsVariable, settings)
    def updateJs(self):
        return '%s.write("%s");' % (self.jsVariable, self.flashId)

HTML_FRAGMENT_TEMPLATE = """
<div id="%(FLASHID)s">
		<strong>
                A Flash based chart should show up here.<br>
                If this message is not replaced with a chart, <br>
                you may need to upgrade your Flash player,<br>
                or the chart may not be configured correctly<br>
                at the web server.
                </strong>
</div>

<script type="text/javascript">
		// <![CDATA[		
	var %(JSVARIABLE)s = new SWFObject("%(SWFFILE)s", "%(FLASHNAME)s", "%(WIDTH)s", "%(HEIGHT)s", "%(FLASHVERSION)s", "%(BACKGROUNDCOLOR)s");
	%(JSVARIABLE)s.addVariable("path", "%(PATH)s");
	%(JSVARIABLE)s.addVariable("chart_data", encodeURIComponent(%(DATA)s));
	%(JSVARIABLE)s.addVariable("chart_settings", encodeURIComponent(%(SETTINGS)s));
	%(JSVARIABLE)s.write("%(FLASHID)s");
</script>
"""
                     

class ColumnChart(Chart):
    "encapsulates chart information for column chart"
    def __init__(self, settings=None):
        self.init_settings(settings)
        self.graphs = []
        self.series = None # only one series allowed [until I understand what multiple series mean...]

    default_swfFile = "amcolumn/amcolumn.swf"
    default_path = "amcolumn/"
    default_swfObjectJs = "amcolumn/swfobject.js"
    
    def addGraph(self, graph):
        self.graphs.append(graph)
    def addSeries(self, series):
        assert self.series is None, "cannot redefine series "
        #pr "series set", series
        self.series = series
    def rectify(self):
        self.series.rectify()
        for g in self.graphs:
            g.align(self.series)
    def dataXML(self):
        assert self.series, "must have a series to format XML data"
        assert self.graphs, "must have a graph to format XML data"
        L = ["<chart>"]
        L.append( self.series.toXml() )
        L.append("<graphs>")
        for g in self.graphs:
            L.append(g.toXml())
        L.append("</graphs>")
        L.append("</chart>")
        return "\n".join(L)
    def fromDict(self, dictionary, env=None, prefix="amchart."):
        self.labelsFromDict(dictionary, env, prefix)
        self.guidesFromDict(dictionary, env, prefix)
        self.settings = getEnvironmentParameters(prefix+"settings.", dictionary.get("settings"), env, strict=False)
        # someday refactor this to remove common functionalities
        # find graph dictionaries sequence
        graphDefaults = getEnvironmentParameters(prefix+"graph.", None, env)
        #pr "graphDefaults", (prefix+"graph.", graphDefaults)
        graphValueDefaults = getEnvironmentParameters(prefix+"graph.value", None, env)
        graphs = dictionary.get("graphs", ())
        gcount = 0
        for g in graphs:
            gid = gcount
            gcount += 1
            if type(g) is not types.DictType:
                g = {"values": g}
            #g.update(graphDefaults)
            g = addDefaults(g, graphDefaults)
            #pr "added", graphDefaults
            #pr "graph", g
            valuesDictionaries = g["values"]
            values = valuesSequence(valuesDictionaries, graphValueDefaults)
            g["values"] = values
            if g.get("gid") is None:
                g["gid"] = str(gid)
            gr = GraphFromDictionary(g)
            self.addGraph(gr)
        seriesDefaults = getEnvironmentParameters(prefix+"series.", None, env)
        seriesValueDefaults = getEnvironmentParameters(prefix+"series.value", None, env)
        series = dictionary.get("series", ())
        if type(series) is not types.DictType:
            series = {"values": series}
            #series.update(seriesDefaults)
            series = addDefaults(series, seriesDefaults)
            valuesDictionaries = series["values"]
            values = valuesSequence(valuesDictionaries, seriesValueDefaults)
            series["values"] = values
            sr = SeriesFromDictionary(series)
            self.addSeries(sr)

class LineChart(ColumnChart):
    "encapsulates chart information for line chart"
    # line charts and column charts are essentially the same from the python perspective...
    default_swfFile = "amline/amline.swf"
    default_path = "amline/"
    default_swfObjectJs = "amline/swfobject.js"

class PieChart(Chart):
    "encapsulates chart information"
    def __init__(self, settings=None):
        self.init_settings(settings)
        self.slices = []

    default_swfFile = "ampie/ampie.swf"
    default_path = "ampie/"
    default_swfObjectJs = "ampie/swfobject.js"
    
    def addSlice(self, slice):
        self.slices.append(slice)
    def dataXML(self):
        assert self.slices, "must have a slice to format XML data"
        L = ["<pie>"]
        for s in self.slices:
            L.append(s.toXml())
        L.append("</pie>")
        return "\n".join(L)
    def fromDict(self, dictionary, env=None, prefix="amchart."):
        self.labelsFromDict(dictionary, env, prefix)
        self.settings = getEnvironmentParameters(prefix+"settings.", dictionary.get("settings"), env, strict=False)
        # find slice dictionaries sequence
        #pieDefaults = getEnvironmentParameters(prefix+"pie.", None, env) # there are no pie parameters
        pieSliceDefaults = getEnvironmentParameters(prefix+"slice", None, env)
        slices = dictionary.get("pie", ())
        for s in slices:
            if type(s) is not types.DictType:
                s = {"value": s, "title":str(s)}
            #s.update(pieSliceDefaults)
            s = addDefaults(s, pieSliceDefaults)
            sl = SliceFromDictionary(s)
            #pr "from", s, "got slice", sl
            self.addSlice(sl)

def formatSettingsTree(tagName, tree, labels=None, guides=None, indent=""):
    nextindent = indent+" "
    L = ["%s<%s>" % (indent,tagName)]
    if labels:
        L.append("%s<labels>" % (indent,))
        for l in labels:
            L.append(l.toXml(nextindent))
        L.append("%s</labels>" % (indent,))
    if guides:
        # XXXX max_min option is not yet supported...
        L.append("%s<guides>" % (indent,))
        for g in guides:
            L.append(g.toXml(nextindent))
        L.append("%s</guides>" % (indent,))
    its = tree.items()
    its.sort()
    for (name, value) in its:
        # XXXX the following asserts are for safety -- better to get an exception than a graph that is broken (?)
        assert name!="labels", "ambiguous use of labels settings "+repr((name,value))
        assert name!="guidess", "ambiguous use of guides settings "+repr((name,value))
        if type(value) is types.DictType:
            fmt = formatSettingsTree(name, value, indent=nextindent)
        else:
            fmt = "%s<%s>%s</%s>" % (nextindent, name, value, name)
        L.append(fmt)
    L.append("%s</%s>" % (indent,tagName))
    #pr "joining", L
    return "\n".join(L)

def settingsTree(settings):
    collector = {}
    items = settings.items()
    items.sort() # so errors can be reproduced...
    for (name, value) in items:
        if "." in name:
            #pr "complex name", (name, value)
            [tag, remainder] = name.split(".", 1)
            tagD = collector.get(tag, {})
            assert type(tagD) is types.DictType, "value setting cannot be redefined as container "+repr((name, value, tagD))
            assert tagD.get(remainder) is None, "cannot redefine setting "+repr((name, value, tagD[remainder]))
            tagD[remainder] = value
            collector[tag] = tagD
        else:
            #pr "atomic name", (name, value)
            assert collector.get(name) is None, "cannot redefine setting (2) "+repr((name, value, collector[name]))
            collector[name] = value
    #pr "collector is", collector
    D = {}
    for (name, value) in collector.items():
        if type(value) is types.DictType:
            D[name] = settingsTree(value)
        else:
            D[name] = value
    #pr "settings tree is", D
    return D

def valuesSequence(valuesDictionaries, defaultAttributes):
    values = []
    for v in valuesDictionaries:
        if type(v) is not types.DictType:
            v = {"value": v}
        #v.update(defaultAttributes)
        v = addDefaults(v, defaultAttributes)
        vl = ValueFromDictionary(v)
        values.append(vl)    
    return values

def getEnvironmentParameters(prefix, dictionary, env, strict=True):
    if dictionary is None:
        dictionary = {}
    if env:
        dictionary = dictionary.copy()
        lp = len(prefix)
        for (name, val) in env.items():
            if name.startswith(prefix):
                suffix = name[lp:]
                if (not strict) or (not "." in suffix):
                    dictionary[suffix] = val
    return dictionary

def SeriesFromDictionary(dictionary, init=None):
    if init is None:
        init = Series
    values = dictionary["values"]
    #attributes = dictionary.copy()
    #del attributes["values"]
    attributes = {}
    for (k,v) in dictionary.items():
        if k!="values":
            attributes[ str(k) ] = v
    return init(values, **attributes)

def GraphFromDictionary(dictionary):
    return SeriesFromDictionary(dictionary, init=Graph)

class Series:
    "encapsulates information for a chart series"
    tag = "series"
    def __init__(self, values, **attributes):
        vL = []
        for v in values:
            if isinstance(v, Value):
                vL.append(v)
            else:
                vL.append(Value(v))
        self.values = vL
        self.attributes = attributes
    def toXml(self):
        contentList = [ x.toXml() for x in self.values ]
        content = "\n".join(contentList)
        content = content.replace("\n", "\n ")
        return xmlContainer(self.tag, content, self.attributes)
    def rectify(self):
        count = 0
        for v in self.values:
            v.defaultXid( str(count) )
            count += 1
    
class Graph(Series):
    "encapsulates information for a graph in a chart"
    tag = "graph"
    def align(self, series):
        count = 0
        nseries = len(series.values)
        for vg in self.values:
            if count<nseries:
                vs = series.values[count]
                vg.defaultXid(vs.getXid())
            count+=1

def ValueFromDictionary(d, init=None):
    if init is None:
        init = Value
    x = d["value"]
    #attributes = d.copy()
    #del attributes["value"]
    attributes = {}
    for (k,v) in d.items():
        if k!="value":
            attributes[ str(k) ] = v
    #pr "init", (init, x, attributes)
    result = init(x, **attributes)
    #pr "init returns", init, result
    return result

def SliceFromDictionary(d):
    return ValueFromDictionary(d, Slice)

class Value:
    "encapulates a value in a series or graph"
    tag = "value"
    def __init__(self, x, **attributes):
        self.x = x
        self.attributes = attributes
    def toXml(self):
        if self.x is None:
            return "" # missing values disappear
        return xmlContainer(self.tag, self.x, self.attributes, False)
    def getXid(self):
        return self.attributes["xid"]
    def defaultXid(self, value):
        if self.attributes.get("xid") is None:
            self.attributes["xid"] = value

class Slice(Value):
    "pie slice -- just another name for a value"
    tag = "slice"

class Label:
    tag = "label"
    def __init__(self, dictionary):
        self.lid = None
        self.dictionary = dictionary
    def toXml(self, indent=""):
        if self.lid is not None:
            L = [ '<%s lid="%s">' % (self.tag,self.lid) ]
        else:
            L = [ '<%s>' % (self.tag,) ]
        its = self.dictionary.items()
        its.sort()
        for (n,v) in its:
            L.append( ' <%s>%s</%s>' % (n,v,n))
        L.append('</%s>' % (self.tag,))
        return indent+(("\n"+indent).join(L))

class Guide(Label):
    tag = "guide"

def xmlContainer(tag, content, attributes, whitespace=True):
    av = attributes.items()
    av.sort()
    avL = []
    for (a,v) in av:
        try:
            v = str(v)
        except UnicodeEncodeError:
            v = unicode(v, errors="ignore")
        jv = quoteValue(v)
        if whitespace:
            avL.append( '\n  %s=%s' % (a, jv))
        else:
            avL.append(' %s=%s' % (a, jv))
    atts = "".join(avL)
    if whitespace:
        fragment = "<%s%s>\n %s\n</%s>" % (tag, atts, content, tag)
    else:
        fragment = "<%s%s>%s</%s>" %(tag, atts, content, tag)
    return fragment

def quoteValue(v):
    return '"%s"' % v.replace('"', '\\"')

def addDefaults(d, defaults):
    result = defaults.copy()
    result.update(d)
    return result

# === testing stuff...

def test0():
    V = Value(100, xid=99, url="http://yahoo.com")
    print "test value"
    print V.toXml()
    S = Series([4,5,6])
    print "test series"
    print S.toXml()

def test1():
    #gdict = {
    #    "settings": {"background.color": "#DADADE", "background.alpha": 88},
    #    "graphs": [ [1,2,3,4,5], [1,5,1,4,1], [2,6,4,4,1] ],
    #    "series": [ 1984, 1184, 1114, 1111, 6666 ],
    #    }
    #C = ColumnChart()
    #C.fromDict(gdict)
    C = testGraph()
    print "test settings"
    print C.settingsXML()
    print "test chart data"
    print C.dataXML()

TESTTEMPLATE = """
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Column & Bar chart</title>
</head>

<body>
<!-- saved from url=(0013)about:internet -->
<!-- amcolumn script-->
  <script type="text/javascript" src="%(SWFOBJECTJS)s"></script>
	<div id="%(FLASHID)s">
		<strong>You need to upgrade your Flash Player</strong>
	</div>

	<script type="text/javascript">
		// <![CDATA[		
		var %(JSVARIABLE)s = new SWFObject("%(SWFFILE)s", "%(FLASHNAME)s", "%(WIDTH)s", "%(HEIGHT)s", "%(FLASHVERSION)s", "%(BACKGROUNDCOLOR)s");
		%(JSVARIABLE)s.addVariable("path", "%(PATH)s");
                //%(JSVARIABLE)s.addVariable("settings_file", encodeURIComponent("amcolumn/amcolumn_settings.xml"));
                //%(JSVARIABLE)s.addVariable("data_file", encodeURIComponent("amcolumn/amcolumn_data.xml"));
	%(JSVARIABLE)s.addVariable("chart_data", encodeURIComponent(%(DATA)s));
	%(JSVARIABLE)s.addVariable("chart_settings", encodeURIComponent(%(SETTINGS)s));
		%(JSVARIABLE)s.write("%(FLASHID)s");
		// ]]>
	</script>
<!-- end of amcolumn script -->
</body>
</html>
"""
        
DOCUMENTSCAFFOLD = """
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Column & Bar chart</title>
</head>

<body>
<h1> chart scaffold </h1>
%(script)s
%(fragment)s
<!-- end of amcolumn script -->
</body>
</html>
"""

DEBUGDOCUMENTSCAFFOLD = """
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Column & Bar chart</title>
</head>

<body>

<h1> welcome to the debug scaffold </h1>

%(script)s
<!-- end of amcolumn script -->
</body>
</html>
"""

DEBUGTEMPLATE = """
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Column & Bar chart</title>
</head>

<body>
<!-- saved from url=(0013)about:internet -->
<!-- amcolumn script-->
  <script type="text/javascript" src="%(SWFOBJECTJS)s"></script>
	<div id="%(FLASHID)s">
		<strong>You need to upgrade your Flash Player</strong>
	</div>

	<script type="text/javascript">
		// <![CDATA[		
		var %(JSVARIABLE)s = new SWFObject("%(SWFFILE)s", "%(FLASHNAME)s", "%(WIDTH)s", "%(HEIGHT)s", "%(FLASHVERSION)s", "%(BACKGROUNDCOLOR)s");
		%(JSVARIABLE)s.addVariable("path", "%(PATH)s");
                %(JSVARIABLE)s.addVariable("settings_file", encodeURIComponent("amcolumn/amcolumn_settings.xml"));
                %(JSVARIABLE)s.addVariable("data_file", encodeURIComponent("amcolumn/amcolumn_data.xml"));
		%(JSVARIABLE)s.write("%(FLASHID)s");
		// ]]>
	</script>
<!-- end of amcolumn script -->
</body>
</html>
"""
def testGraph():
    gdict = {
        "settings": {"background.color": "#DADADE", "background.alpha": 88},
        "graphs": [ {"title": "pigs", "values":[1,2,3,4,5]},
                    [1,5,1,4,1], [2,6,4,4,1] ],
        "series": [ 1984, 1184, 1114, 1111, 6666 ],
        "labels": [ { "y":20, "align":"center", "width":300, "text": "this is the label text"} ],
        "guides": [ { "start_value": 1.2, "end_value": 3.9, "fill_color":"#000000,#CC0000"}],
        }
    env = {
        "amchart.graph.bullet": "square",
        "amchart.settings.outline_color":"#550055", 
        "amchart.settings.outline_alpha": 95,
        "amchart.settings.animation.start_time": 2,
        "amchart.settings.background.color": "#005555"
        }
    C = LineChart()
    C.fromDict(gdict,env)
    C.rectify()
    return C

def testPie():
    pdict = {
        "settings": {"background.color": "#DADADE", "background.alpha": 88},
        "pie": [9,{"value": 6, "title":"pigs", "url":"http://whiff.sourceforge.net", "description":"whiff link"},],
        }
    env = { }
    P = PieChart()
    P.fromDict(pdict, env)
    return P

def testSettings():
    C = testGraph()
    print C.settingsXML()

def testData():
    C = testGraph()
    print C.dataXML()

def testPage1():
    from whiff.rdjson import jsonParse
    C = testGraph()
    D = {}
    D["JSVARIABLE"] = "colvariable"
    D["DATA"] = jsonParse.format(C.dataXML())
    D["SETTINGS"] = jsonParse.format(C.settingsXML())
    D["SWFFILE"] = "amcolumn/amcolumn.swf"
    D["PATH"] = "amcolumn/"
    D["SWFOBJECTJS"] = "amcolumn/swfobject.js"
    D["FLASHID"] = "flashColChart"
    D["FLASHNAME"] = "flashycolchart"
    D["WIDTH"] = "520"
    D["HEIGHT"] = "400"
    D["FLASHVERSION"] = "8"
    D["BACKGROUNDCOLOR"] = "#FFFFFF"
    result = TESTTEMPLATE % D
    print result

def testPage2():
    C = testGraph()
    C.specifications("colvariable", "flashColChart", "flashycolchart", 520, 400)
    D = {}
    D["script"] = C.swfObjectScript()
    D["fragment"] = C.htmlFragment()
    return DOCUMENTSCAFFOLD % D

def testPiePage():
    from whiff.rdjson import jsonParse
    P = testPie()
    P.specifications("piechartvariable", "flashPieChart", "flashypiechart", 520, 400)
    D = {}
    D["script"] = P.swfObjectScript()
    D["fragment"] = P.htmlFragment()
    return DOCUMENTSCAFFOLD % D

if __name__=="__main__":
    #test0()
    test1()
    #testPage1()
    #print testPage2()
    #print testPiePage()
    #testSettings()
    #testData()


